Repository navigation
Starter-kit authoring loop, one KPI dashboard, reverse-engineer references (PRD-750/751/735) - #575
Conversation
…1) and the reverse-engineer rename (PRD-735) Owner interview 2026-10-26: one iteration = in-repo kit author + cold sandbox build, retro on both transcripts; KPIs K1 cold-build cost, K2 first-build quality, K3 friction, K4 AI play grade; ranked genre list; PRD-735 lands first and renames threenative-game-refs to threenative-reverse-engineer-mcp; perf:live becomes the single pnpm dashboard; one epic PR. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…Asset perf tabs perf:live becomes pnpm dashboard. The CI speed and Asset perf tabs render their ledgers on each load (missing = pending, malformed = invalid). The two static docs/*/index.html pages, the loops' report/serve commands and their consumers go. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…engineer-mcp The data repo moved and was renamed (the old release URL redirects). The engine MCP reads the new release and files contributions there. A request that asks to reverse engineer a game leads with the three reference tools, and their descriptions carry the words. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…oop tab scripts/kit-loop.mjs turns a cold-build dir (transcript, gates, friction, play grade, retro) into one ledger row with K1-K4; any missing input fails closed. Row 1 is the baseline and sets the K1/K3 targets. The hub renders docs/kit-loop/ledger.json as the Kit loop tab, and the native-performance-loop skill now adds a hub tab instead of a per-campaign page. PRD-751 is done. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
PRD-750 Phase 1 + PRD-751 Phase 2 landed (
PRD-750 Phase 2 waits for PRD-749 (kit base) to land on |
… (gitleaks full-history scan)
…ed cards) with the data repo proof
…uild: K1 97,840 tokens, K2 21/22 + CLOSE, K3 7, K4 4); dashboard default port 4751
|
PRD-750 iteration 1: baseline ledger row 1 recorded (
Top frictions for iteration 2: the performance gate goes red under foreign GPU load while the capture lock reports no competitor; the engine MCP is absent because Still open: AC-3 waits for the PRD-749 kit base (only its Phase 1 contract landed; collectathon is red on Also in this push: PRD-735 AC-1 (3,287-row Tier-2 import) and AC-3 (Ocarina of Time, 17 reviewed cards) are ticked, with the proof in the data repo. |
…hem and says rerun, not cut the game (PRD-750 retro friction 1) Kit-loop row 1: the cold agent spent ~25 turns lowering the high tier's look to pass a performance gate that two foreign Playwright GPU processes were loading, while the capture lock printed 'no competing runner detected'. foreignGpuProcesses() reads /proc for Chromium GPU processes outside this run's tree; the failure stays a failure (fail closed), its message names them and its suggestion says to rerun when they exit.
…gent in the project so .mcp.json loads the engine MCP (PRD-750 retro friction 2) Kit-loop row 1: the cold agent scaffolded from its session folder, so the engine MCP that the project's .mcp.json wires never loaded and it searched no capability (it grepped agent-docs).
…hat never loaded .mcp.json (PRD-750 retro friction 2) node node_modules/@threenative/core/mcp/engine.mjs <tool> '<json>' prints the tool text (exit 0) or names the error (exit 1). The scaffold's last line names it beside 'start your agent in the project'. Red-green: server.spec 'command line', scaffold.spec completion.
…laytest.json) (PRD-750 row 2 retro)
Kit-loop row 2: the cold agent quoted a brace list and got TN_PLAYTEST_SCENARIO_UNREADABLE, then
reran the whole suite. segmentMatches now reads one level of {a,b}. Red-green: bridgeClient.spec.
…es AGENTS.md, read one (PRD-750 retro friction 3) Rows 1 and 2 both read the 45 KB CLAUDE.md + AGENTS.md pair, then AGENTS.md again. Red-green: scaffold.spec completion.
…CLOSE, K3 2, K4 4) names row 1's fixes; AC-5 notes
|
PRD-750 iteration 2: ledger row 2 recorded TLDR: with row 1's retro fixes in the tarballs, the same cold SM64 prompt cost 55% fewer tokens, finished in 15 turns, and passed all 22 gates. The agent changed no code.
Fixes that landed before row 2's author step:
Row 2's retro fixes landed for row 3:
Label: |
… AC-4 (rows 2-3 gate-green cold builds on the kit); rows 4..N blocked on the PRD-749 kit base
- the scaffold's engine fallback names the project's absolute path (a session started in the parent folder got MODULE_NOT_FOUND from the relative one) - foreign GPU processes are Playwright Chromium only (an editor's or desktop browser's GPU process made every real over-budget failure read as contended) - an unmeasured cost is never called contended (the suggestion said rerun beside 'cannot be judged') Red-green: frame-budget.spec, scaffold.spec completion.
- every lookup word of every shipped kit routes 'make me a game like <word>' to its own kit first - the core engine shim answers one tool call from a folder outside the project (exit 0, collectathon first) and an unknown tool exits 1 with no stdin hang
…budget met and a CLOSE verdict The Kit loop tab marked K2 met on rows 2-3 from 22/22 scenarios while their frame budget was over. Red-green: kit-loop.spec.
|
Kit loop tab with ledger rows 1–3 (headed Chromium, 0 console errors,
|
…legated); rows 1-3 recomputed Presented p95 on the runner's private Xvfb is display cadence (17.5 ms with a 10 ms game). gates.json now carries costP95Ms (fails closed when missing); rows 2-3 meet the budget at 10.03 and 12.59 ms, row 1 stays over at 17.27 ms. Red-green: kit-loop.spec.
… the kit-loop KPI id in the full-history gitleaks scan
#579) * chore(refs): point references at ThreeNativeHQ/threenative-reverse-engineer-mcp [skip ci] The data repository moved from jonit-dev/threenative-game-refs to ThreeNativeHQ/threenative-reverse-engineer-mcp. The old name still redirects, but search, the contribution command and the docs should name the canonical repository. The default URL already tracks `releases/latest`, so published data keeps flowing without a version pin. `packages/core/mcp/engine-server.mjs` is regenerated from the engine-mcp build; its only delta is the same two constants. * fix(refs): read the catalog fields a reference release now carries [skip ci] `index.json.gz` game rows gained `kind`, `rating`, `genre` and `subgenre`, and the strict row whitelist rejected the whole release with `TN_REFERENCES_MALFORMED` — the engine could not read any release published after the catalog grew those columns. Search never reads them, so the whitelist accepts them and drops them; a field the engine does not use must not gate a data release. Templates also name classic-game mechanics references next to capability search in the authoring skill list, so a scaffolded agent sees both discovery paths. Proof against the published v0.2 release: `THREENATIVE_REFERENCES_URL=https://github.com/ThreeNativeHQ/threenative-reverse-engineer-mcp/releases/download/v0.2/ pnpm exec vitest run packages/engine-mcp/__tests__/references.spec.ts` (28 passed, live release download included).
…only Node global (native shim gate)
…ing-loop # Conflicts: # packages/create-threenative/__tests__/scaffold.spec.ts
…ps with a ready reference search (PRD-750, sandbox-4 made no engine call)
… the top mechanics; a ready command alone was skipped (PRD-750 sandboxes 4-5)
…-authoring-loop # Conflicts: # packages/create-threenative/__tests__/scaffold.spec.ts
…-authoring-loop # Conflicts: # packages/create-threenative/__tests__/scaffold.spec.ts
|
Merging into Full run 40487627667 on
The run before the base merge (40476691039 on
PRD-750 stays open at prd:25%: AC-3 and AC-5..7 wait for the PRD-749 kit base. |
…n + render thread on by default (PRD-613) (#577) * docs(PRD-613): resume the render thread lane after placement * docs(PRD-613): order ctest and TSan proof for the render thread * fix(runtime-native): native asset loader reports core's asset cache (PRD-743 surface) PRD-743 added IAssetLoader.cache, prefetch and startBackgroundFill, and the playtest bridge's state sample reads cache.enabled. The native player's loader had none of them, so every native playtest observation threw TN_INSPECT_GAME_BRIDGE (reading 'enabled'), and a game passing assets.cache or assets.backgroundFill threw TN_NATIVE_ASSET_OPTIONS_UNSUPPORTED. The native loader now reports the cache as disabled with core's native reason, and prefetch and the fill resolve at once. Red-green: native_engine_player_imports red on the options refusal, green after. * perf(PRD-613): batching holds a projecting decision while the scene's structure does RenderDatabase::batch judged a projecting projection decision again every frame: a full scene walk, content classes and the group maps, only to read the rigs' verdicts. On Midway that was about 11% of main-thread samples at cockpit and rear gunner. The decision now holds while hierarchyVersion and structureStamp (moved from the bindings into projection/plan.h, shared with hostProjectionReport) hold, and while every rig's lane key (lane reason, similarity, mirror sign) holds; the 60-frame cadence still re-judges it. TN_BATCH_DECISION_CACHE=0 (diagnostic) restores the old cadence for paired arms. ctest: skinned_projecting_cadence red then green; skinned_crowd_cpu caught the rig-scale and material cases; native-engine label 516/517 (target_graph is the UI-overlay configure). * perf(PRD-613): the render thread is on by default Phase 3 passed on Midway at 99ad82d: main-thread CPU per frame falls by 3.4-7.5 ms per pose in three alternating pairs (render thread plus the held batch decision against today's develop path), parity PASS in every run, no steady-state fallback, visual judge NEUTRAL at all three poses. TN_RENDER_THREAD=0 turns it off; a GPU timing run keeps it off, since it has no synchronized timing snapshot (TN_RENDER_THREAD=1 with GPU timing still fails closed). Red: a Midway flight with the variable unset on the binary before this change reports no renderThreadCpuMs. * docs(PRD-613): done; the default render thread passes its green flight dflt-green (render thread on with no flag, after the switch) reports renderThreadCpuMs in 4,574 frame lines, fallbacks 1, parity PASS; dflt-red on 99ad82d reports none. The PRD moves to docs/PRDs/done/. PRD-610 AC-16 stays open for the owner: cull and project stay on the main thread. * style: format scaffold.spec.ts comment indent (lint red on develop since #579) * chore(PRD-613): refresh native coverage report for the render database and player changes * ci: allowlist the kit-loop KPI field id false positive on #574/#575 branches The merge-group secrets scan reads every fetched ref, so a verified false positive on another open branch stops every queued PR. Same entries as prd/temporal-upsampling bc25d08, plus the copy on pr563-followup.
…the merge-group commit that adds it has no SHA to pin The merge queue stopped #574 on supply-chain: gitleaks generic-api-key on scripts/kit-loop.mjs:45, the KPI field id k2ScenarioPct, already verified as a false positive and pinned by fingerprint on #575's and #574's branch commits. The squash brings the file to develop in a merge-group commit whose SHA exists only once the queue builds it, so no fingerprint can name it. The inline allow suppresses that one line; the scan and its config are unchanged. Checked with the pinned CI image (gitleaks dir on the file): 1 leak before, no leaks after.
…n, kit contract and worker mode (PRD-723/746/747/748/749/752) (#574) * fix(runner): the jump clears a block The apex was 7.2^2/38 = 1.36 m, so the hitbox's bottom (feet + 0.05 m) never rose past a 1.5 m block's top: every jump over a block was a crash, and the advertised space-to-jump did nothing useful. Take-off is now 8.5 m/s: a 1.9 m apex, clear of the block for 0.44 s (4.8 m at the 11 m/s start speed). Jump and hitbox constants are exported for the spec. Red (__tests__/runner.spec.ts): clearance discriminant -3.26 (never clears). Green: 1/1 passed. PRD-747 * fix(runner): count a near miss only beside a real obstacle The near-miss loop tested every lane of every chunk, not the obstacles in it: a lane change on empty road, the first 48 m included, scored near misses and shook the camera. Chunks now list their obstacles and the loop tests those while the runner is level with one. Red (playtests/near-miss.playtest.json): nearMisses 3 after three lane changes in the first 44 m. Green: 0. PRD-747 * fix(minimal): the follow camera stays in front of the arena wall (PRD-747) render/camera.ts followed a fixed offset with no wall check, so with the player's back to a wall the lens passed behind it and the wall filled the frame (free play, frames 9-10). Layer: kit (src/render/camera.ts, framing only; no colour or look value). followCamera now takes a WallProbe (the collectathon pattern): a wall between the aim and the offset pulls the lens in front of it at once, and the shortened arm climbs (the adventure kit's rule) so the lens looks down on the player; it eases back out when clear. Play.ts registers 'camera' with debug().hidden (a wall between player and lens). Needs 62b225d79: before it, the probe hit the player's own capsule. Red-green: playtests/camera-wall.playtest.json (walk back into the south wall; hidden equals false) red before: hidden false -> true; green after: pass. Visual judge (fresh read-only Opus, 2 headed WebGPU runs per arm, NVIDIA Turing, ~/Downloads/pr574-prd747-cam/): IMPROVEMENT overall; at-wall and walk IMPROVEMENT, spawn and away NEUTRAL; note: the top-down pose at the wall foreshortens the mannequin. * test(collectathon): drop the HUD 'covered' count that could not fail (PRD-747) Play.ts set statePatch.hudFaded = under, then counted 'under && !statePatch.hudFaded': always 0, so enemies.playtest.json's 'covered equals 0' asserted the line above it. Layer: kit (proof only, no behaviour change). The count and its assertion go; 'under >= 1' still proves a Sunseed sat under the pill, and Hud.tsx fades on state.hudFaded. Check: playtests/enemies.playtest.json passes in the scaffold after the change. * fix(sailing): the end panel names the loss, and its clock and wind hold where the run ended Running out of wind was reported as 'capsized', and under the end panel the elapsed clock kept counting and the wind kept draining. The HUD now reads the passage's clock and wind as they stood when it ended; the sea keeps its own clock. Red: fail.playtest elapsed 3.20 s two seconds after a capsize (bound <= 1.1); float-after-the-run-ends outcome panel 'capsized' after the wind ran out. Green: elapsed 1.00 s; outcome 'out of wind' (and 'capsized' after a capsize). PRD-747 * fix(sailing): take the capsize test key out of the players' hint The menu advertised 'C capsize test', a developer hook that ends the run. The binding stays for playtests/fail.playtest.json and is commented as such; the hint now reads 'WASD / arrows steer and trim'. Text only, no behaviour change. PRD-747 * fix(puzzle): the V replay keeps the shove count and reports its bodies After V the crates were rebuilt with their rest pose set to the post-V pose, so 'shoved' and 'push metres' read 0 for a vault the player had already pushed; replayBodies was read after the rig was cleared, so it was always 0. Red (playtests/replay-keeps-progress.playtest.json): pushedCrates 0 after V (1 before it), replayBodies 0. Green: pushedCrates 1, replayBodies 3. PRD-747 * test(create-threenative): re-pin scaffold hashes for the PR #574 ARPG review The Three patch ships in every scaffold, so its shared-skin history fix moves all sixteen trees; with the previous patch bytes only action-rpg moved (checked). create-threenative tests: 116 files, 1836 tests pass. * feat(shooter): generated tiling surfaces on the town's procedural geometry (PRD-746) Five greyscale tiles generated through scripts/reference.mjs (google/gemini-3.1-flash-lite-image, 512 px WebP) and three derived normal maps, 313 KB in all, recorded in bay-PROVENANCE.md. src/render/surface.ts takes only each tile's structure (sample over its own mean), so the 7b546d6 neutral palette keeps its colour; a turned large-scale copy breaks the repeat, wall feet darken, roughness follows the texture and the fine detail fades with distance. Town surfaces stay standard node materials, so the high-tier backlight still converts them; the plain grid materials remain the fallback. Fresh Opus judge vs the step-1 frames: IMPROVEMENT (9 of 11 poses, deck undersides NEUTRAL). * fix(tower-defense): a pooled walker no longer respawns with a chilled health bar Symptom: after a cryo tower, walkers in later waves showed the cyan 'chilled' bar while moving at full speed. Root cause: src/enemies/Enemy.ts reset() cleared the slow but kept the bar tint a chilled death left behind. reset() now restores barFull. Enemy.barTintMatches + battle.barTintMismatches report the invariant. Red-green: playtests/chill-tint.playtest.json (new; cryo + sentry, auto waves at 3x, 30 samples), headed WebGPU nvidia/turing. Before: components FAIL, barTintMismatches up to 8. After: exit 0, 36/36 samples 0 through wave 4. * docs(prd-748): plan performance by construction, the engine decides the fast path and fails closed (owner chose option 1) * fix(rts): A arms attack-move without panning the camera Symptom: pressing A to arm attack-move slid the view left, so the follow-up click landed on different ground. Root cause: src/game.ts bound KeyA to both 'move' (camera pan) and 'attack'. The pan now uses the arrow keys only, as the letter keys are orders. Red-green: playtests/rts-attack.playtest.json now asserts state.cameraX stays -70 at box-release and armed. Headed WebGPU nvidia/turing. Before: FAIL resource.state.cameraX.atSteps (-71.2 after A). After: cameraX -70 at every sample; the run's only red is TN_SCENE_WARNING under host load 65 (unverified, a host-load reading, not this change). * docs(prd-748): evidence from the kit perf inventory; Phase 1 gains stage attribution, the allocation guard and explore-path bounds * fix(rts): the minimap draws units where they are and the real camera view Symptom: every unit dot sat in the top-left 28 px of the tactical overview, and the view box was a fixed-size square that did not follow zoom or the isometric angle. Root cause: src/ui/Minimap.tsx drew minimapDots (tenths of a cell) as raw pixels, and derived the view from cameraX/Z/zoom with the wrong scale. Play.ts now publishes minimapView, the ground under the frame's four corners in the same units, and Minimap draws dots and that quad scaled by cell. Red-green: playtests/rts-build.playtest.json adds a visual region over the fogged top-left corner of the map. Headed WebGPU nvidia/turing. Before: FAIL visual.0.region.darkPixels 0.910 < 0.98. After: exit 0, ratio 1.0. * docs(prd-749): one kit base and a kit contract, fix a bug once and prove it on every kit (owner ask) * refactor(platformer): read the fox's leg by name, keep fox.ts under 200 lines (PRD-747) looks.spec.ts caps each generated render file under 200 lines; 650e60724 took render/fox.ts to 205. The legSwing getter leaves IFox: the left leg group is named 'legL' and Fox caches it for standingSwing (NaN, so the idle assertion fails closed, if the name goes). No behaviour change. Checks: looks.spec.ts 21/21 pass; playtests/idle.playtest.json passes in the scaffold; scaffold tsc, eslint and biome clean. * test(minimal): runtime-cost sentinel passes followCamera a wall probe (PRD-747) 0a6ea3067 made the WallProbe a required followCamera argument; template-runtime-cost.spec.ts called it with three. The sentinel now runs both arms (clear way, and a wall at 1 m that pulls in and climbs) and still expects zero vector clones or constructors. Check: template-runtime-cost.spec.ts 7/7 pass. * fix(puzzle): the HUD shows the player's progress, not the vault's physics telemetry The top-left panel showed developer telemetry to players: seed, solid and phase crate counts, at rest, push metres, blocked ticks, walked through, seal contacts, and replay bodies/drift in exponential notation. It now shows the title and 'crates shoved', plus 'replay matched/drifted' after V. The telemetry stays in the published state the scenarios read. Visual judge (fresh Opus, headed WebGPU nvidia/turing, 2 runs per arm): IMPROVEMENT overall; readability NEUTRAL, rest of frame NEUTRAL (no regression). Frames: ~/Downloads/pr574-prd747-puzzle-hud/. PRD-747 * docs(prd-748,749): default-on perf capabilities audit; agent docs describe the kit base and contract (owner ask) * test(create-threenative): re-pin scaffold hashes for the PRD-747 kit fixes Recomputed 2026-10-26 (PRD-747 kits): collectathon, minimal and platformer gain play fixes and playtests; values measured by scaffold.spec.ts itself. Checks: packages/create-threenative/__tests__ 116 files / 1836 tests pass; scripts/check-core-boundary.ts passes. * fix(rts): rolling the wheel away from you zooms in Symptom: the mouse wheel zoomed the opposite way to the other kits and to the genre: wheel up pulled the camera out. Root cause: src/scenes/Play.ts passed the zoom axis (positive for a wheel rolled away from the user) straight to zoomRts, whose notches grow the view height. The call now negates it. Red-green: playtests/rts-zoom.playtest.json (new). Headed WebGPU nvidia/turing. Before: FAIL resource.state.cameraZoom.atSteps, closer = 32.4 (expected 27.6). After: exit 0, 30 -> 27.6 -> 30. * docs(rts): list the rts-pause and rts-zoom scenarios * fix(racing): a car the player stopped is parked, not beached The stall rescue counted any car under 0.6 m/s for 1.2 s, with or without throttle. A player who braked to a stop on the road was rescued every 1.2 s, and the fourth rescue is a DNF: five seconds parked ended the race (found in free play: DNF at 00:06.2). A stall now counts only while the engine pushes, which is the pinned-against-a-hoarding case the rescue exists for. Red (playtests/stopped-car-is-not-rescued.playtest.json): rescues 4, raceStatus DNF after a 7 s stop. Green: rescues 0, RACING; rescue, outcome and reverse scenarios still pass. Unit (racing.spec.ts): parked car shouldRescue true -> false; driven stuck car still true. PRD-747 * fix(rts): a finished match shows its result and can be played again Symptom: at the end of a match the DEFEAT/VICTORY tag sat under the command column's hint bar (bottom right), half hidden, and nothing could start a new match short of reloading the page. Root cause: src/ui/Hud.tsx put the result in the pause button's corner stack, and no intent restarted the game. The result is now a centred panel with a 'play again' button; src/game.ts maps the 'restart' intent to game.goto('play'), which resets state and the scene. Proof: out-of-tree five-minute scenario (box select, gather, train, wheel, arrow pan, attack-move, pause, all army, base; 17,000 ticks) until the AI wins, then the button. Headed WebGPU nvidia/turing. Before: DEFEAT clipped under the hint bar, no restart control. After: exit 0; result 'defeat' -> '', supplyUsed 0 -> 23, clock 04:01 -> 00:03. Not added to playtests/: it is 4x the longest kit scenario. * docs(rts): name the zoom axis sign by DOM deltaY * PRD-749 phase 1: the kit contract suite, sightLine and hudAudit assertions Layer: tooling (playtest package, template sweep). A bug found in one kit becomes one row in packages/playtest/src/kit-contract.ts; scripts/kit-contract.ts runs every row on every kit from test:templates and prints a kit-by-row matrix. Kits declare what rows read in template-playtests/contract.json (never shipped to a game); a row a kit neither declares nor opts out of with a written reason fails closed. Rows: explore-clean, camera-sees-subject, hud-clean, no-hitch, foot-slip, restart-resets, action-clip-completes, effect-follows-contact. Two new assertions back them: sightLine (camera ray to the subject, sampled at the path cadence) and hudAudit (debug text and text contrast from the DOM). Red-green: sight-line.spec (10), hud-audit.spec (11), kit-contract.spec (7), scripts kit-contract.spec. Browser runs of the rows are held; the per-kit red list follows. * test(scaffold): re-pin the kart, puzzle, racing, runner and sailing trees Recomputed 2026-10-26 (PRD-747 kits) from a clean committed tree: the play fixes in this lane move those five scaffolds. The byte-stability test passes (1/1) with the new values. PRD-747 * test(create-threenative): re-pin the rts and tower-defense scaffold hashes Recomputed 2026-10-26 (PRD-747 kits) by scaffold.spec.ts on this branch's tree. * test(kart): motion blur off (owner: the kart seems to hang while driving) * feat(core): AnimationPlayer.play takes a one-shot speed, like Godot's play(name, blend, speed) Layer: engine. A one-shot always played at its authored rate, so the action-rpg roll (a 1.47 s Roll clip) was cut at its 0.5 s dodge and blended back to the legs mid-dive, which read as a weird jump. `speed` sets the rate of that play of a "once" clip; the next play returns to 1; a loop keeps its stride-synced rate. A non-positive or non-finite speed throws. Red-green: animation.spec "plays a one-shot at the requested speed..." (x 0.25 vs 0.5) and "rejects a play speed..." red before, green after; animation, animation-rate and animation-weighted specs 134/134 pass. Written by the DeepSeek arm from my red test; reviewed. * fix(action-rpg): the roll plays its whole clip, travels and faces where it goes Layer: kit. Player.dodge held the 1.47 s Roll clip for 0.5 s, so the legs took over mid-dive (pelvis at 0.65 m, falling from a 1.13 m leap) and the roll read as a cut-off jump. A roll with no key held did not move, and a roll toward a new direction played facing the old one, because the heading is frozen while a one-shot holds. The roll now holds 0.7 s and plays the whole clip inside it (Fighter.play `fit`, on core's new one-shot `speed`), picks its direction on its first tick from the stick or the facing, turns the figure to it and carries the body along it. The snapshot reports `facingDegrees`. Red-green: playtests/roll.playtest.json, 4 failures before (moved 0.42 m, facing 180 not 270, action not dodge at 0.6 s, second roll refused), green after; combat, swing and win green. Written by the DeepSeek arm from my red scenario; reviewed (its hit-hold change reverted). * feat(templates): the starter kits share one 0 EV exposure meter (PRD-747) Core stays stock Unreal 5.8.3 at +1 EV (key 0.36, r.DefaultFeature.AutoExposure.Bias 1.0, UE 5.8.3: Engine/Source/Runtime/Engine/Private/SceneView.cpp:196-199). The owner chose one shared kit override: template-assets/kitExposure.ts sets key 0.18 with Unreal's -10..+20 EV100 range around it, registered in SHARED_RENDER_SOURCES and stamped into adventure, collectathon, kart, platformer, racing, runner, sailing and starter by pnpm stamp:template-render. The shipped config equals the judged K arm (key 0.18, minStops log2(0.18)-20, maxStops log2(0.18)+10): a fresh Opus judge read IMPROVEMENT vs CURRENT on 9 of 9 kits, 2 runs per arm, headed WebGPU on nvidia/turing. Minimal (owner: CURRENT is best), rain, shooter, snow, tower-defense, puzzle, rts and action-rpg keep their own exposure. kit-exposure.spec.ts: each shared kit ships the canonical copy byte for byte and meters at key 0.18 on every tier; minimal, rain, snow keep 0.36 and shooter 0.14. Scaffold hashes re-pinned for the eight kits. * fix(action-rpg): the dungeon returns to the owner's 7b546d6 neutral look as named overrides of core Owner 2026-10-26: "check exposure issue too, same as the shooter". The core-look migration made the floor and walls read warm beige: sun 0xfff0dc x5.5, floor 0x98948e, sky 0.8/0.45, filmic at metered exposure. Restored as named overrides on core's qualityPreset (`neutralLook`): ACES at fixed exposure 0.62, the kit bloom and GTAO, 4x MSAA; sun 0xfff1e0 x4.5, floor 0xb4b1ae, horizon 0xacb1c1, full sky range. The effective preset equals the 7b546d6 kit's on every tier (only the unused SSGI/SSR radii differ). Later material fixes (props) stay. Meter on CURRENT (2 runs, nvidia/turing): 0.002 to 0.092 stops, luminance 0.34-0.36, no pumping; startup frames step 0.60 -> 0.52 mean brightness about 1.3 s after the reveal. RESTORED: one step. Judge (fresh Opus): RESTORED vs CURRENT IMPROVEMENT in all three rooms (yellow cast removed); RESTORED vs PRE-MIGRATION MATCH room 1, CLOSE rooms 2-3 (camera pose offset only). kit-look-defaults.spec asserts the overrides: 87/87 pass. * fix(core): a stride-synced walk in the 32-128 px band evaluates every frame Owner 2026-10-26: "leg movement and actual movement looks weird too". The auto update rate held the top-down action-rpg hero (about 60 px) at band rate 2 while it ran: the pose evaluated every other frame on a body that moves every frame, so the planted foot rode with the body. Measured on the walk scenario: 0.97 m of foot travel per metre at band rate, 0.50 at rate 1 (nvidia/turing). A moving, stride-synced walk in that band now evaluates every frame; a still idle, smaller rigs and explicit updateRate keep their rate. Red-green: animation-rate.spec "evaluates a stride-synced walk every frame in the 32-128 px band" (red: rate 2), 156/156 across the animation specs. * fix(kart): every kart pitches with the road between samples, so no kart flutters (PRD-747) Owner: "KART IS SHAKING WHILE MOVING" and "all karts shaking/fluttering not only player". Cause: render/kart.ts set each model's pitch from course.slope[kart.index], the slope of the nearest 1 m centre-line sample, so the pitch stepped once per metre for every kart. Fix: Course.slopeAt interpolates the slope between samples, as heightAt already does for height. Red: smooth-drive.playtest.json, shakePitch player 1.3088 deg and cpu.1 1.3603 deg per frame (bound 1). Green: player 0.3253, cpu.1 0.1781; all assertions pass. TN_TEMPLATE_ONLY=kart test:templates: 12 of 13 pass, smooth-drive included. Only production-performance fails (GPU p95 37.68 ms on a loaded host, hardware adapter). Scaffold spec 74/74, kart hash re-pinned. Fix by the DeepSeek arm, reviewed by the lead. * fix(shooter): no motion blur, a body that eases to speed and stops, an eye that climbs stairs smoothly (PRD-746) Owner: "Blur is even weird, character movement is weird", then "Feel free to tweak that motion blur or disable it. check unreal codebase, search on github for some decompilation for a decent FPS". - Motion blur off on every tier. Core's is Unreal's (UE 5.8.3: Engine/Source/Runtime/Engine/ Private/Scene.cpp:646-648) and scales with frame time (Renderer/Private/SceneVisibility.cpp: 5650-5658), so a mouse turn smeared the frame, and the skinned hands took the camera's motion (Unreal keeps them sharp with first-person velocity, Shaders/Private/VelocityShader.usf:74-82). - movement.ts: friction, then acceleration toward the wished speed (Quake III bg_pmove.c PM_Friction / PM_Accelerate mechanics, no code copied). The old code set the velocity in one tick. - The eye climbs a stair step at a fixed rate instead of jolting with the body (the Half-Life view smoothing). The rifle bob follows the body's speed. Proof: shooter-movement.spec.ts plus related specs 166/166. Playtests on NVIDIA Turing, headed: movement-eases and stairs-smooth-the-eye exit 1 on the old code and exit 0 on the new one (speed 3.67 while sliding to a stop, viewRisePeak 0.0291 <= 0.064). The old-code failures are missing observations (speed and viewRisePeak did not exist), so the old values are not measured. TN_TEMPLATE_ONLY=shooter test:templates: 35/36 pass; only performance fails (p95 36.19 ms on a loaded host). Visual judge for the blur change: not run. Written by lane-shooter-feel (stopped), verified by the DeepSeek arm, reviewed by the lead. * Revert "fix(physics): a ray from inside a capsule skips the capsule (PRD-747)" This reverts commit 62b225d79080725420a60042c10b26c5c16cae39. * test(scaffold): re-pin the 16 kit trees after merging the PR #574 kit lanes * Reapply "fix(physics): a ray from inside a capsule skips the capsule (PRD-747)" This reverts commit 60aca2c86971c7d9667fe47bd99b9a27d12bfe87. * fix(runtime-native): a native ray from inside a capsule or ball skips it, as on web (PRD-747) The native host skipped a solid only at time of impact 0 with a zero normal. Rapier gives a box a zero normal there but a ball or capsule the reversed ray, so a ray from inside a character's capsule hit the capsule itself. Native now skips on toi 0 alone and reports a zero normal for a hit at toi 0, the same as packages/physics (62b225d79). Containment was rejected: Rapier 0.19.3 containsPoint is true on the boundary, so it cannot separate "on the surface" from "inside". Red: cargo test 23 passed, 1 failed (capsule/ball case, lib.rs:2741). Green: 24 passed. Web: a ball case added; packages/physics vitest 29 files, 240 tests pass. * chore(pr574): format the merged kit-contract files, fit create-threenative AGENTS.md under 100 lines, sync mirrors, count 19 platformer playtests * test(pr574): re-pin 5 kit hashes after the biome format pass * test(native-engine): the TRAA invariant-scope test counts the per-shape velocity program; refresh the native coverage record (PRD-746) * fix(shooter): the hands walk on Walk and sprint on Run at their authored rate, with no procedural bob on top (PRD-747) * PRD-752: run web games in a worker so the page cannot stall them Files Cycle v5 of PRD-573 as its own PRD (worker mode, Wasm threads), records the owner's 2026-10-26 worker HUD decision and clears the PRD-573 blocker. * profile:wasm-page --load runs a page under a stated CPU load (PRD-752) Keeps N busy-loop processes alive for the whole page run and stamps the count on the report, so a loaded-host baseline is reproducible. * docs(prd-748): the engine owns the sim budget, query cost, tick significance and HUD fields, from the shooter profile * docs(prd-573): Phase v2.7, the web bridge's JS side under 2% of the frame (owner request, shooter profile) * Core reads window and document only through platform.ts; loaded-host baseline (PRD-752) - check-core-boundary type-checks packages/core/src against the webworker lib and fails on any value read of a global a worker lacks outside platform.ts (red on 19 reads in 5 files, green after the move). - platform.ts gains hostWindow/hostDocument/hostResizeObserver; audio, game, hot, renderer and viewport use them. The renderer now throws TN_RENDERER_NO_CANVAS instead of a ReferenceError with no document. - profile:wasm-page reports main-thread busy percent. - PRD-752 phase 1 baseline and v5 KPIs recorded; owner scope decision. * UI bridge loopback over a MessageChannel behind ?tnUiLoopback=1 (PRD-752) Both ends stay in one realm but every frame arrives a task later, the hop a game in a worker adds. Installed as the host globals by whichever end connects first, so the game and the HUD do not change. * fix(kart): a spun or tumbled kart lifts by its own dip instead of rotating half under the road (PRD-747) * PRD-752 phase 2 done: native desktop gate and starter loopback proof * fix(ui): DebugOverlay reads the registry snapshot only while it is open (PRD-748) A snapshot calls every entity's debug(). The closed panel polled it every 100 ms in every dev build, and the shooter's soldiers measure bone contacts there: 63% of sampled main-thread time in a DevTools trace. Red: the new overlay spec saw snapshot() called with the panel closed. * perf(shooter): soldiers check the grip on the rifle's own box, sight rays test only the boxes they cross, HUD panels read their own fields (PRD-748) Paired profile, one scaffold, 2 runs per arm, RTX 2080: main thread busy 937/956 -> 840/769 ms/s; Enemy.update, boneContact and React re-renders leave the top of the profile. Shooter suite 36/36. The exact boneContact walk still runs whenever the cheap gap exceeds the 5 cm grip bound. * fix(ui): DebugOverlay reads the registry snapshot only while it is open (PRD-748) A snapshot calls every entity's debug(). The closed panel polled it every 100 ms in every dev build, and the shooter's soldiers measure bone contacts there: 63% of sampled main-thread time in a DevTools trace. Red: the new overlay spec saw snapshot() called with the panel closed. * docs(prd-748): the loading cover hides page UI by default; debug-only work costs nothing while off * Engine side of the worker boot: platform hook, worker engine bundle, named WebGPU failure (PRD-752) A scratch starter booted in a worker on an OffscreenCanvas on the Wasm engine. These are the engine-layer fixes that spike needed: - core: defineGame takes a platform source a worker host installs (__tnGamePlatform) when the portable entry names none. - core: TN_NO_RENDERER names the WebGPU failure instead of swallowing it. - create-threenative: worker bundles get the engine resolver and ES output. - runtime-native: the web module is built for web and worker environments. - three-native: the renderer tolerates an OffscreenCanvas (no setAttribute). - PRD: the spike result and the phase 3 design. * fix(kart): models draw between fixed ticks, a hit costs 8 box corners not a vertex walk, and the HUD names the item key (PRD-747, PRD-748) - Judder: ctx.interpolate(model.root), as the camera already is; a model that only stepped stood a tick ahead of the chase camera (0.44 m at speed). Owner confirmed on screen: 'kart lgtm'. - Hang: the sink meter walked every vertex of a hit kart each fixed step (105 ms). DevTools, 15 s held throttle with a hit: red 19 frames over 100 ms, green 0 frames over 50 ms (max 33 ms). - Items: E / X shows under the held item; new item-key playtest presses E for the player (boxes 0 -> 1, used '' -> 'nitro'). Before it only the autopilot ever used an item. Kart suite 14/15; production-performance (GPU p95 35-42 ms vs 16.7) was red on this lane before these changes and stays open. * Worker game host: @threenative/core/worker runs a game in a worker on an OffscreenCanvas (PRD-752) serveWorkerGame (worker) and startWorkerGame (page) speak a small message contract: a ready handshake before the canvas transfer, the platform source the game boots on, page-relative URLs, forwarded keyboard/pointer/wheel input in canvas coordinates, the UI bridge on its own port, and the playtest bridge mirrored onto the page as async calls. 9 linked-pair tests. * Worker game audio: the page's AudioContext plays a worker game's sound (PRD-752) A worker has no Web Audio API. The worker gets a stand-in AudioContext whose nodes are handles; calls and property writes cross a MessagePort in one batch per task and run on the page's real graph, so three's audio classes and AudioBus work unchanged. AudioBus finds its gesture target in a worker, canvas gestures reach it as they would by bubbling, and a page gesture resumes the page's context. 7 remote-audio tests, 2 worker-host tests, 1 platform test. * feat(quality): a ThreeNative ESLint plugin: tree walks and GPU resources in frame callbacks are errors, useUiState() without a selector warns (PRD-748) * fix(engine): sibling renderers no longer hand a texture change back and forth every frame (PRD-748) A renderer that saw a sibling's texture change bumped the shared generation again, so the next renderer saw a change too. Once one texture changed, every render of every render target dropped its map groups and render bundles for the life of the process: the shooter made 54 bind groups and 26 render bundles a frame. Seeing a change now only records it. Red: renderer_caches_test sibling_texture_change failed on the old code. Shooter, 3 paired 10 s profiles: onRender 420 to 354 ms/s, busy 931 to 813 ms/s. * Worker game HUD: the UI bridge works when the game connects while its module loads (PRD-752) The starter's game.ts calls game.ui.onIntent at top level, so in a worker the game end connected before the page's port arrived and stayed on the in-process broker: a blank HUD. @threenative/core/worker now installs the game end's host globals when it loads in a worker, before the game module evaluates, and holds frames each way until init attaches the port. 3 tests; headed GPU probe shows the full HUD on the worker page. * Starter: worker.html runs the same game in a web worker (PRD-752) src/worker.ts serves the unchanged game.ts; src/worker-page.ts keeps the canvas, input and the same GameUi on the page. index.html stays the default, so one build carries both arms of the busy-page A/B. Headed GPU probe: same frame and HUD as index.html. * profile:wasm-page times frames on the game's own thread (PRD-752) A worker page's requestAnimationFrame times the page, not the game. The probe now reads frame intervals in the worker that holds the game's platform source, else on the page, and names that thread in the result (gameThread). Main-thread busy stays the page's CPU profile. 1 test. * fix(shooter): shot vessels burst into drawn shards again; Breakables reads the shard's live material (game layer, template) Root cause: createMaterialLighting swaps every scene mesh to a converted material copy made at enable time (shard opacity 0), so the cached shard.material that Breakables mutated was no longer the material drawn; shards wrote depth and GTAO but no colour. drawn(shard) reads mesh.material at use. breakable-shatters gains breakables.drawnShards >= 7: red 0 (broken 1, liveShards 9), green 9. Shooter scaffold hash re-pinned. * Worker game pointer lock: mouse look works for a game in a worker (PRD-752) An OffscreenCanvas cannot lock the pointer. In a worker its requestPointerLock and exitPointerLock ask the page, which locks its own canvas within the click's activation window and forwards each pointerlockchange with the page's verdict as captured; a refused lock is reported as captured false. InputMap takes that flag where no document exists, unchanged on a page. 2 tests. * test(shooter): the idle hands case calls locomotion with its two arguments (typecheck was red since 7f0cd699b) * docs(prd-748): a material the game holds stays the material that draws (shooter vase shards) * profile:wasm-page times the frames the game presents (PRD-752) A worker's requestAnimationFrame does not follow what the game presents, and on a loaded page rAF ticks at 60 Hz while the game presents far fewer frames. The probe now also times the game's getCurrentTexture calls (the gpuCalls divisor) on the game's own thread, in both arms, as presented; it shims tsx's __name in the game worker, and fails closed with a timeout. 1 test. * Shooter: worker.html runs the same game in a web worker; PRD-752 decisions (PRD-752) The shooter kit gets the starter's worker page: src/worker.ts serves the unchanged game.ts, src/worker-page.ts keeps the canvas, input and GameUi. Headed probe on the RTX 2080: town, soldiers, rifle and the live HUD with minimap and clock draw from the worker. The PRD logs gaps 7 to 9, the presented-frame timing, the loaded-host rule and the measured busy share (starter 0.9 to 1.9% on worker.html against 45.7 to 86.9% on the page). * Worker game playtest bridge: mirror it while start is held for the runner (PRD-752) With a runner announced, the playtest plugin holds the boot until the runner calls the bridge, so a mirror sent after start never reached the page and the harness reported TN_PLAYTEST_BRIDGE_MISSING. The worker now posts each mirror the moment the game installs the global. 1 test. * fix(core): stride sync leaves a camera-carried rig at its authored rate; the shooter hands no longer play at 3x (PRD-747, PRD-748) Layer: engine. AnimationPlayer stride sync (default on) read the camera carrying first-person hands as ground covered: walking measured 5.6 m/s against the clip's 0.017 m/s of hand sway, and a mouse turn swept the root 5.9 m/s, so the hands played at the 3x ceiling - the owner's 'seizure', inconsistent because it followed how much he moved and turned. Every first-person game had it. - Unset strideSync now skips a rig under a camera; strideSync: true still syncs it; stride.cameraCarried reports it, and the measured rate stays live (overridden: true). - animation-camera-stride.spec: red 2/2 (rate 3, no report field), green 2/2; animation specs 10 files 178/178. - Live shooter probe at 60 fps (core dist swapped in a served copy): walk rate 3 synced -> Walk 1.226 cycles/s vs 1.22 authored, synced false; idle while turning 0.76 = authored. - Rifle.debug() reports handsRate; playtests/hands-authored-rate asserts it <= 1 while walking (red/green run queued on the GPU slot, result follows). * feat(ui): page UI reads nothing behind the loading cover; useUiStateDuringStartup is the reported override (PRD-748) * test(scaffold): re-pin the shooter hash for the hands rate guard * fix(engine): a render target's renderer no longer draws an output image nothing reads (PRD-748) * Worker is the default page for the starter and shooter kits (PRD-752) index.html now runs the game in a web worker; main.html is the named override that runs it on the page thread. The worker canvas forwards setAttribute to the page canvas, so the harness reads the renderer's data-engine for capture provenance (was TN_PLAYTEST_CAPTURE_PROVENANCE_MISSING). 1 test. * fix(core): the camera cull stops walking the camera's children when the engine culls (PRD-748) * test(shooter): hands-authored-rate measures the clip seconds the hands really ran, walking and firing The first guard read the action's effective time scale, which the rifle resets each tick, so it passed on the broken core too (1 vs 1). Rifle.handsRate now integrates the playing clip's real advance per game second. Walking then firing (the Shoot clip the rifle does not re-time): old core 2.83 (fail), fixed core 1.00 (pass), same served copy, core dist swapped. Shooter hash re-pinned; scaffold spec 74/74; scaffold tsc 0. * docs(prd-748): record the render-cost attribution, three fixes and the open structural boxes * Engine raycast: a cached triangle BVH per geometry (PRD-752) Mesh::raycast tested every triangle of the mesh. The shooter's merged town meshes hold about 430k triangles, so a few soldier line-of-sight rays cost about 90 ms in one update, which was the stutter left on the worker page. The tree is built on first use, rebuilt when positions, index or drawRange change, and skipped for morphed meshes and meshes that pose their vertices (SkinnedMesh). Candidates run the unchanged hit code in triangle order, so results stay identical: 2,048 oracle rays, 0 differ. 1,000 rays on a 20k mesh: 8,471 ms -> 6.0 ms; a focused ray tests 4 of 20,000 triangles. * feat(core): static meshes that share a material merge into one draw by default (PRD-748) AutoBatch merges settled static meshes per material once the scene stops changing, hides the sources instead of removing them, and gives a source back when the game moves, re-materials or removes it (or an ancestor). Rays test the merged mesh's hierarchy and still name the authored mesh. Named overrides, all reported by TN_AUTO_BATCH: userData.batch = "off", autoBatch: false, autoBatch: { guard: "everyFrame" }. The shooter drops collapseTown. * Worker mode: fix the review findings in the worker host, remote audio and profiler (PRD-752) Worker: startup errors reject ready, stop removes the canvas and releases pointer lock and capture, pointerlockerror, 0x0 clamp, HUD wheel stays on the HUD, motion coalesced per page frame, one HUD snapshot in flight, gamepads forwarded, a lazy playtest mirror with _clockEpoch. Audio: the clock freezes while suspended, fluent AudioParam calls reuse their handle, ended one-shots release theirs, numeric param defaults, control ops settle from the page. Profiler: probes run in the game realm. Each item has a red-green spec. * profile:wasm-page --vsync keeps the browser's frame pacing for worker A/Bs (PRD-752) The default flags unthrottle the page's requestAnimationFrame, not a worker's OffscreenCanvas frames, so a worker arm read 37 ms frames against 10 ms on the page. With vsync on, the shooter worker ran 16.7 ms frames against 33.3 ms on the page thread. Also types the memory probe's realm (Page | Worker union). * fix(playtest): the HUD audit composites a parent's background under the child's plate; kart race panels sit on plates (PRD-749) Layer: playtest harness, plus the two kits it then judged correctly. - sampleHudAudit walked up the DOM and painted each ancestor OVER the nearer plate, so an opaque page background hid every HUD plate. hud-audit-sampling.spec (browser-backed): red 100 vs 40, green. sampling + hud-audit specs 48/48. - Kart: LAP, TIME, place and speed sat bare on the bright sky (orange at 1.45:1); each corner panel now sits on the kit's panel colour at 60%. Checked by eye on a played frame. - Shooter: the legend separators rise from white/25 (2.12:1) to white/50. hud-clean contract row, shooter + kart: FAIL/FAIL -> ok/ok. Kart and shooter hashes re-pinned; scaffold spec 74/74. * fix(core): auto batch merges at scene enter, and warm-up and projection skip hidden sources (PRD-748) Merging at enter() draws no unmerged frame and lets the first warm-up count 589 merged meshes instead of 8866 sources. A group's last giveBack no longer queues an upload for geometry it is about to dispose (createBindGroup error on scene restart). Projection and warm-up ignore sources the batch hides. Projection tests opt out with autoBatch: false. * Playtest: a worker game relaunches without the unlimited frame rate (PRD-752) Under a private Xvfb the runner adds --disable-frame-rate-limit so a page game's frames escape the compositor's cadence. The same flag throttles a worker's OffscreenCanvas frames (shooter: 36 ms presented with it, 16.7 ms without). The runner now sees a worker game once the page opens and relaunches once with that thread's flags; the report says gameThread: worker. * PRD-752: the worker KPI box, measured with vsync on both kits * profile:wasm-page spec: record the launch args without a comma expression (PRD-752) * Playtest: a live-clock hold counts the ticks the game ran, not the requests (PRD-752) A wall-clock advance answers a one-tick request with the ticks the host's pump ran: two per frame at 30 fps on a 60 Hz step. advanceFixedStep counted requests, so a 169-tick key hold ran about 338 ticks and the starter's player overshot into the gap on the worker page. The bridge client now returns the reported count and the loop counts it; a fixed-step bridge still runs exactly one tick per request. Starter play passes on / (worker) and /main.html. * PRD-752: starter and harness boxes ticked; worker-in-Xvfb and live-clock hold decisions * PRD-752: visual judge NEUTRAL ticked; shooter performance on the real display matches the page thread * PRD-752: no worker-only frame rate under Xvfb (scheduler probe, draw-call evidence) * PRD-752: phase 4 ceiling measured (shooter about 6%, Midway about 25% splittable engine work) * PRD-752: phase 4 threaded-build probe (1-5% single-thread cost); job pool waits on PRD-573 v3.1 * PRD-752: threaded-build cost re-measured on one source tree (about 1.5-3%); earlier A/B void * docs(prd-748): tick the loading-cover box with the shooter page and worker proof * fix(kits): adventure, collectathon and rain HUD text reaches 3:1 (PRD-749) The hud-clean kit-contract row failed on four kits of the merged #574 branch. Direct headed WebGPU runs (NVIDIA adapter), worst ratio before -> after: - adventure: keys count "0" (text-lume) 2.33 -> 3.70, chest hint (text-text/70) 2.70 -> 3.90. HUD panel and toast bg-panel/40 -> /60. - collectathon: coin, gem and sunseed counts (text-lume) 2.15 -> 3.58. HUD pill bg-panel/25 -> /50 (gems still show through), toast /40 -> /60. - rain: compass letters "NW 355° NE" 2.75 -> 3.88. .compass colour alpha 0x60 -> 0x80. - minimal: TN_PLAYTEST_HUD_AUDIT_UNOBSERVED is true: the kit draws no DOM text during play (main.ts only prepends the canvas). It opts out of hud-clean with a written reason. Scaffold hashes re-pinned for adventure, collectathon and rain. * Starter-kit authoring loop, one KPI dashboard, reverse-engineer references (PRD-750/751/735) (#575) * docs(prd-750): plan the kit authoring loop, one KPI dashboard (PRD-751) and the reverse-engineer rename (PRD-735) Owner interview 2026-10-26: one iteration = in-repo kit author + cold sandbox build, retro on both transcripts; KPIs K1 cold-build cost, K2 first-build quality, K3 friction, K4 AI play grade; ranked genre list; PRD-735 lands first and renames threenative-game-refs to threenative-reverse-engineer-mcp; perf:live becomes the single pnpm dashboard; one epic PR. Co-Authored-By: Claude Opus 5.5 <[email protected]> * PRD-751 Phase 1: pnpm dashboard is the one KPI hub with CI speed and Asset perf tabs perf:live becomes pnpm dashboard. The CI speed and Asset perf tabs render their ledgers on each load (missing = pending, malformed = invalid). The two static docs/*/index.html pages, the loops' report/serve commands and their consumers go. Co-Authored-By: Claude Opus 5.5 <[email protected]> * PRD-735 AC-0: references come from ThreeNativeHQ/threenative-reverse-engineer-mcp The data repo moved and was renamed (the old release URL redirects). The engine MCP reads the new release and files contributions there. A request that asks to reverse engineer a game leads with the three reference tools, and their descriptions carry the words. Co-Authored-By: Claude Opus 5.5 <[email protected]> * PRD-750 Phase 1 + PRD-751 Phase 2: pnpm kit:loop record and the Kit loop tab scripts/kit-loop.mjs turns a cold-build dir (transcript, gates, friction, play grade, retro) into one ledger row with K1-K4; any missing input fails closed. Row 1 is the baseline and sets the K1/K3 targets. The hub renders docs/kit-loop/ledger.json as the Kit loop tab, and the native-performance-loop skill now adds a hub tab instead of a per-campaign page. PRD-751 is done. Co-Authored-By: Claude Opus 5.5 <[email protected]> * PRD-735: tick AC-1 (Tier 2 import, 3287 rows) and AC-3 (OoT 17 reviewed cards) with the data repo proof * PRD-750 iteration 1: baseline ledger row 1 (collectathon, SM64 cold build: K1 97,840 tokens, K2 21/22 + CLOSE, K3 7, K4 4); dashboard default port 4751 * fix(playtest): a cost bound failed beside other GPU processes names them and says rerun, not cut the game (PRD-750 retro friction 1) Kit-loop row 1: the cold agent spent ~25 turns lowering the high tier's look to pass a performance gate that two foreign Playwright GPU processes were loading, while the capture lock printed 'no competing runner detected'. foreignGpuProcesses() reads /proc for Chromium GPU processes outside this run's tree; the failure stays a failure (fail closed), its message names them and its suggestion says to rerun when they exit. * fix(create-threenative): the scaffold's last line says to start the agent in the project so .mcp.json loads the engine MCP (PRD-750 retro friction 2) Kit-loop row 1: the cold agent scaffolded from its session folder, so the engine MCP that the project's .mcp.json wires never loaded and it searched no capability (it grepped agent-docs). * feat(engine-mcp): one tool call from the command line for a session that never loaded .mcp.json (PRD-750 retro friction 2) node node_modules/@threenative/core/mcp/engine.mjs <tool> '<json>' prints the tool text (exit 0) or names the error (exit 1). The scaffold's last line names it beside 'start your agent in the project'. Red-green: server.spec 'command line', scaffold.spec completion. * docs(prd-750): AC-5 notes: row 1's two retro fixes landed, row 2 pending * fix(engine-mcp): type runCli's io sink as void so the spec typechecks; rebundle * fix(playtest): --scenario takes a shell brace list (playtests/{a,b}.playtest.json) (PRD-750 row 2 retro) Kit-loop row 2: the cold agent quoted a brace list and got TN_PLAYTEST_SCENARIO_UNREADABLE, then reran the whole suite. segmentMatches now reads one level of {a,b}. Red-green: bridgeClient.spec. * fix(create-threenative): the scaffold's last line says CLAUDE.md copies AGENTS.md, read one (PRD-750 retro friction 3) Rows 1 and 2 both read the 45 KB CLAUDE.md + AGENTS.md pair, then AGENTS.md again. Red-green: scaffold.spec completion. * PRD-750 iteration 2: ledger row 2 (K1 44,483 tokens -55%, K2 22/22 + CLOSE, K3 2, K4 4) names row 1's fixes; AC-5 notes * PRD-750: ledger row 3 (K1 47,107, K2 22/22 + CLOSE, K3 0, K4 4); tick AC-4 (rows 2-3 gate-green cold builds on the kit); rows 4..N blocked on the PRD-749 kit base * fix: review findings on the PRD-750 retro fixes - the scaffold's engine fallback names the project's absolute path (a session started in the parent folder got MODULE_NOT_FOUND from the relative one) - foreign GPU processes are Playwright Chromium only (an editor's or desktop browser's GPU process made every real over-budget failure read as contended) - an unmeasured cost is never called contended (the suggestion said rerun beside 'cannot be judged') Red-green: frame-budget.spec, scaffold.spec completion. * test: authoring-layer regression guards from the PRD-750 kit loop - every lookup word of every shipped kit routes 'make me a game like <word>' to its own kit first - the core engine shim answers one tool call from a folder outside the project (exit 0, collectathon first) and an unknown tool exits 1 with no stdin hang * fix(kit-loop): K2 reads met only with all scenarios green, the frame budget met and a CLOSE verdict The Kit loop tab marked K2 met on rows 2-3 from 22/22 scenarios while their frame budget was over. Red-green: kit-loop.spec. * PRD-750: K2 frame budget is the performance gate's cost p95 (owner delegated); rows 1-3 recomputed Presented p95 on the runner's private Xvfb is display cadence (17.5 ms with a 10 ms game). gates.json now carries costP95Ms (fails closed when missing); rows 2-3 meet the budget at 10.03 and 12.59 ms, row 1 stays over at 17.27 ms. Red-green: kit-loop.spec. * ci(prd-750): record the PRD-735 reference-tool descriptions and allow the kit-loop KPI id in the full-history gitleaks scan * Hook reference search to the new data repository and its release shape (#579) * chore(refs): point references at ThreeNativeHQ/threenative-reverse-engineer-mcp [skip ci] The data repository moved from jonit-dev/threenative-game-refs to ThreeNativeHQ/threenative-reverse-engineer-mcp. The old name still redirects, but search, the contribution command and the docs should name the canonical repository. The default URL already tracks `releases/latest`, so published data keeps flowing without a version pin. `packages/core/mcp/engine-server.mjs` is regenerated from the engine-mcp build; its only delta is the same two constants. * fix(refs): read the catalog fields a reference release now carries [skip ci] `index.json.gz` game rows gained `kind`, `rating`, `genre` and `subgenre`, and the strict row whitelist rejected the whole release with `TN_REFERENCES_MALFORMED` — the engine could not read any release published after the catalog grew those columns. Search never reads them, so the whitelist accepts them and drops them; a field the engine does not use must not gate a data release. Templates also name classic-game mechanics references next to capability search in the authoring skill list, so a scaffolded agent sees both discovery paths. Proof against the published v0.2 release: `THREENATIVE_REFERENCES_URL=https://github.com/ThreeNativeHQ/threenative-reverse-engineer-mcp/releases/download/v0.2/ pnpm exec vitest run packages/engine-mcp/__tests__/references.spec.ts` (28 passed, live release download included). * fix(playtest): the HUD audit compares nodeType to 3, not the browser-only Node global (native shim gate) * fix(create-threenative): a game named with --genre opens its next steps with a ready reference search (PRD-750, sandbox-4 made no engine call) * fix(create-threenative): --genre runs the reference search and prints the top mechanics; a ready command alone was skipped (PRD-750 sandboxes 4-5) * docs(prd-750): record the reference-trigger probes (sandboxes 4-6) * style(create-threenative): sort the scaffold spec imports --------- Co-authored-by: Claude Opus 5.5 <[email protected]> * fix(core): auto batch keeps a still parent merged when its children hold their own matrices (PRD-748) stampMatches reuses one scratch buffer, and writeStamp skipped the matrix slots for an object with matrixAutoUpdate on, so they kept the last stamped object's matrix. The shooter turns matrixAutoUpdate off on its town pieces (sight boxes), so the town group read as moved and the guard gave back all 8307 sources: 13838 draws, CPU p95 89 ms. writeStamp now zeroes the slots. auto-batch.spec 'keeps a still parent merged...' red (and 3 later tests red through the shared buffer), green after; auto-batch + game + constraints 93/93. Shooter performance.playtest, headed WebGPU, real display: no give-back; draws median ~150 (162 before auto batch, collapseTown), one 774-draw frame at the window start (several 796-draw frames before). maxDrawCalls 320 still fails on that startup spike, which predates this lane. * restart-resets: bound running clocks with atSteps lte; platformer gets an R restart (PRD-749) A restart settles 30 ticks before the row samples, and racing elapsed (Race.ts:327), sailing elapsed (Sailing.ts:255) and runner distance (Run.ts:175) keep running in that window, so 'equals 0' could never hold (0.517 s, 0.517 s, 5.16 m observed). Resource atSteps takes lte; contract.json bounds them below the dirty run (2.0 s, 3.0 s, 22 m). Platformer had no restart key at all (the row pressed an unbound KeyR): it now binds R like kart/puzzle and shows the hint. Its spawn sits on the first coin (Play.ts:218 vs Stage.ts:52), so a fresh run reads coins 1; the reset check uses the run clock instead. * explore-clean: judge the path walked, not the net displacement (PRD-749) Starter's seeded explorer walked 63 m and 47 m of odometer, jumped 7 times and fell off three times; the last life lost respawned it on the spawn point, so net distance read 0.003 m and the row called a moving subject stuck. A path is never shorter than the net distance, so no kit that passes now can fail, and step-boundary samples keep physics jitter from counting as travel. * action-clip-completes: judge each clip at its own step; action-rpg publishes its animation and no longer cuts the swing (PRD-749) Three faults stacked behind TN_PLAYTEST_ANIMATION_NOT_OBSERVED on action-rpg: - harness: assert.animation read only the last sample, so a kit with three actions could never pass; it takes atStep now (labeled gameplay series, as footSlip does) and the row uses it. - kit: no entity published 'animation' (the bridge reads entity.animation, see shooter Enemy.ts:758), so nothing was observed. Fighter now publishes the last one-shot action and whether it ran its whole clip (a flinch is a reaction and never replaces it). - kit: the swing held 0.8 s of a 1.533 s Sword_Attack clip (Player.ts SWING), so the follow-through was cut at 52 percent (finished false). It is fitted like the roll; the blade lands at SWING_CONTACT / rate, 12.5 ticks, so contactTicks reads 13. Declared ticks are the hold the kit keeps (48, 42, 42), not the file length: the cast holds 0.7 s, and the roll press at 36 ticks was refused while the cast was still busy. * scaffold spec: re-pin action-rpg and platformer hashes after the restart and clip fixes * fix(playtest): sightLine no longer counts cutouts, effect volumes or ropes as walls A cutout (alphaTest), a transparent surface that writes no depth and a sliver the ray crosses in under 15 cm do not hide the subject. Walls, glass and a camera inside a solid still block. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> * fix(kits): the camera no longer hides the player in starter, platformer, puzzle, action-rpg and adventure starter and platformer pull the lens in front of a solid (ctx.raycast); puzzle and action-rpg cut the wall nearest the lens to a kerb; adventure's camera arm also tests the roots and boulders the physics world does not hold. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> * fix(playtest): a sliver is a rope or a rail only, a thin panel still hides the subject The sliver rule skipped any solid the ray crossed in under 15 cm, so an 8 cm panel stopped counting as a wall. It now also needs the mesh to be thin on two axes. Spec red (panel not blocked) -> green. The reference now says one ray, as the code casts. * action-rpg swing scenario: the fitted swing lands near tick 10, so the landing step starts after one spam press (PRD-749) * scaffold spec: re-pin action-rpg, adventure, platformer, puzzle and starter after the PRD-749 row lanes * style: biome format the camera lane's kit files; re-pin platformer, puzzle and starter * docs(prd-748): auto batch box progress: census numbers and the NEUTRAL judge verdict * docs(prd-749): contract suite box ticked (16 kits, opt-out rule spec); seed rows progress on 6ef214594 * test(native-engine): the offscreen GPU timer sample has three passes since a render target records no output pass (a0ecc6f0d) * docs(native): refresh the native coverage record after the develop merge * docs(prd-749): platformer camera row experiments and why none shipped * build(web-engine): the ray BVH and geometry generators build at -Os on web, so the engine fits its 4 MiB budget again The merged branch read 4,200,028 bytes in CI (allowed 4,194,304). Native keeps -O3. Web engine now 4,145,430 bytes. * fix(test): lift the camera-cut exposure rooms over the black bucket (PRD-747) PRD-747 (4a8cb1cd4) drops meter blocks under Unreal's -10 EV floor. The fixed camera-cut rooms put the dark room at about -9.7 log2 with its backdrop under -10, so the meter ignored part of it and an 11-stop cut read 9.75 stops (verifySnap: terminal target does not match the cut). Both rooms now sit 3 stops higher; the 11-stop ratio, lights and assertions are unchanged. Local snap proof: cut 11.003 stops, gain-0 negative control still fails as expected. * perf(core): auto batch counts scene adds and removes from graph events, not by reading children each frame The pass polled the scene's shape every frame by reading scene.children and each child's children. On the native web engine every children read wraps each child across the bridge, so the shooter town's 8,000 meshes cost about 10 ms of a 21 ms worker frame (owner trace 2026-10-27: 40% of worker CPU). three's childadded/childremoved events now count the same change; a still frame reads no children list. Paired DevTools traces on the shooter route: worker frame interval p50 22.2/23.1 -> 16.7/16.7 ms, p95 29.9/28.9 -> 16.9/17.7 ms. * perf(native-engine): the renderer's batch decision leaves hidden subtrees out RenderDatabase::batch re-decides its projection every 60 frames when the scene holds a CPU-skinned rig. decide() walked and grouped every mesh, hidden ones too, so the 8,300 sources core's auto batch keeps hidden in the shooter town cost a 25 ms Wasm stall once a second (two missed vsyncs). The renderer now decides on what it draws (decide drawnOnly); hostProjectionReport keeps the JS-parity walk. plan_test: 341 sources counted before, 40 after. Paired DevTools traces on the shooter route (auto-batch fix in both arms): frames over 34 ms 10/6/9 -> 1/3, worker interval p99 37.3/37.6/36.2 -> 19.7/24.4 ms. Web Wasm 4,145,393 bytes. * perf(core): the five-second projection window line no longer walks the scene formatProjectionWindow never printed resultDrawCandidates, yet each window read the full report, whose count walks the whole scene. On the native web engine that walk crosses the bridge per node: 8,900 nodes in the shooter town, 13-16 ms on the window frame, a missed vsync every five seconds. SceneRenderProjection.windowReport is the report less that one field; report still counts it. Paired traces: window-frame report cost 14.6/12.9/15.6 ms -> not measurable (under 3 ms) in 2/2 runs. * style(core): sort the projection spec imports * fix(test): lift the plain exposure room one stop over the black bucket (PRD-747) PRD-747 drops meter blocks under Unreal's -10 log2 floor. The plain fixture room's dark backdrop metered -10.07, so dark-adapted read 0.0031594 against the pinned 0.0013968 (TN_EXPOSURE_METER_RANGE). The room now sits one stop higher; three stops washed the unadapted fixed-exposure frame to a blank white (TN_CAPTURE_BLANK). The backlit patches keep their 10 and 14 stops over the room. Re-measured pins (turing, local): dark 0.0027937 (2.000x), sunlight 5.7215 (2.001x). Tolerances and assertions are unchanged. * feat(core): the frame meter reports drawn frames beside presented The web engine's frame gate declines a draw while two frames are on the GPU. TN_FRAME_BUDGET now carries drawn (interval between drawn frames), drawnFrames and drawnFps next to presented, so every window line shows the frames the player lost (shooter: 270 presents against 293 rAF ticks while presented p95 read 16.8 ms). presented and fps are unchanged, so no gate changes its verdict; gating on drawn frames moves to PRD-752 phase 4 with its Xvfb note. * fix(test): core spec fixtures match the projection window and drawn-frame types render-camera-cull passed resultDrawCandidates, which ProjectionWindowReport omits since 9e38c0edf; scene-warning's window lacked the drawn fields from da486c188. Root tsc failed on both. * docs(agents): make performance mistakes painful to write: engine first, fail closed, then a kit lint rule Owner, 2026-10-27. The root rules and the game-facing performance skill name the order: the engine makes the pattern cheap, then fails closed with the call site, then the kit quality ESLint plugin rejects the rest. Kit hashes re-pinned for the skill text. * fix(core): the worker's six unknown casts carry their waiver reasons The quality gate (scripts/__tests__/quality-json.spec.ts) failed on CI run 40576902353: six suppression-class casts from worker mode (PRD-752) had no reason. Each one now says why the type cannot be expressed. * fix(native-engine): the pruned matrix walk defers a hidden node's own compose Core's MatrixWorldPass contract: a hidden node defers its compose and its subtree until shown. The native walk composed the hidden node itself, so the 8,300 hidden auto-batch sources of the shooter paid a compose every frame. A camera and a node holding bones keep composing. Red-green: scene_hierarchy_test pruned_walk fails before (lines 1396-1397), passes after; ctest 168/168 on build/wasm. Web Wasm 4,145,416 bytes (budget 4,194,304). Shooter worker, MCP Brave, vsync on, load 16-23, sceneUpdate span mean A 2.40/2.06 ms -> B 0.43/0.43 ms, p95 4.0/3.0 -> 0.8/0.7 ms. (cherry picked from commit dca708bd49ac0df2f31e883247d4deb0a4ce5a29) * perf(templates): the backlight graph reads its controls once per render Each of the 8 uniform onRenderUpdate callbacks validated the controls, and keyDirection/keyGain each resolved the key: 17 control reads per render, 525 bridge reads per frame on the shooter. One validate and one key resolve per render now feed every uniform; the values each uniform gets are unchanged. Red-green: backlight-shared-graph 'one render reads the controls once' read rimGain 17 times before, <= 5 after. Scaffold hashes re-pinned for the 12 kits (77 specs pass). Shooter worker trace, MCP Brave, vsync on: validateControls + resolveKey inclusive 0.93 -> 0.48 ms per frame (profiler on). (cherry picked from commit 22c706b69f3b5a0ffbb221188ca17c0d6d152e11) * docs(native): refresh the native coverage record after the matrix walk and batch decision changes * fix(build): the desktop UI build bundles src/ui alone, not the kit's main.html game A worker kit's vite.config.ts names index.html and main.html as inputs. The UI build driver merged its own input into that config and then let vite load the config file a second time, so main.html bundled the whole game and its Wasm engine into the HUD build: TN_WASM_ENGINE_MISSING on a desktop build that never needs the web engine. (cherry picked from commit 9e3aab6d605b124fcb70c37e07a42e72a1dcd743) * fix(test): the hot-reload lane proves the starter's page-thread entry, main.html PRD-752 made index.html run the starter in a worker. The provenance sweep then waited for a WebGPU context on a canvas handed to an OffscreenCanvas, and the spec's synchronous __THREENATIVE__.hot() reads met the worker's async mirror. State-keeping hot reload lives in src/main.ts only: Vite cannot hot-update a worker, and once the worker page loads, every game edit fully reloads every page of that server. After an update the restarted game's HUD reads nothing until its startup cover lifts (PRD-748), so the wait includes the score. (cherry picked from commit 6f6e1cd630ac49770e20da0299fba5281fd7be59) * docs(perf): record the 2026-10-27 shooter campaign levers and the crossing, meter and 60 Hz lessons * fix(budgets): worker-only browser globals carry native allowlist reasons; shooter convention cells point at the moved calls CI run 40584406363 failed budgets: TN_NATIVE_SHIM_MISSING for MessageChannel, queueMicrotask and EventTarget, read only by the browser worker page (PRD-752) and the ?tnUiLoopback=1 loopback, and three PRD-289 table cells that the shooter revamp had moved. pnpm budgets now exits 0. * perf(three-native): the web engine keeps what game code reads again, and names what it cannot On the default web engine every read of an engine property crosses JS to Wasm. The shooter's backlight read the key light's intensity 450 times and its colour 225 times a frame, and walks read children and parent the same way. The back end now keeps those answers, so game code that is free in plain three.js is free here too: - JS-written properties (labels, render flags, light and fog parameters, material flags and map slots) stay in a JS mirror until their own setter or a method on the object runs. - Mixer-animated numbers (light intensity, camera fov/zoom/near/far, material opacity and the PBR numbers) stay mirrored until any AnimationMixer/AnimationAction call, which writes them. - children, parent and a whole-subtree walk stay until Object3D::hierarchyVersion moves. The engine-internal getter __hierarchy gives the counter's address once, so the check is a memory read, no crossing. - Layers.test runs in JS on the two in-memory masks. What the cache cannot keep, a census frame per 120 safe points counts by property and names the caller (bridge-meter.ts); past 1/50 of the frame it prints TN_BRIDGE_WARNING, which a playtest fails on. __THREENATIVE_BRIDGE_CACHE__ = false turns the cache off: the paired A/B knob. The hot-callback-scene-work lint rule now also covers TSL onRenderUpdate/onFrameUpdate/ onObjectUpdate, setAnimationLoop and array-iterator callbacks inside a hot callback, and flags iterating .children there (the AutoBatch fingerprint shape). Red-green: bridge-reads.spec.ts 4/7 red on the old back end, 7/7 now; quality-rules 2 cases red on the old rule; perf-warnings bridge case red on the old evaluator. Real Wasm: smoke and GC TN_BROWSER_BACKEND_OK / TN_BROWSER_GC_OK. Web Wasm 4,146,105 B (budget 4,194,304). Paired shooter census (unfixed kit, worker, NVIDIA turing, headed :0): 2,022 -> 661 crossings per frame. Route A/B, 3 runs per arm: crossings 3,249/3,092/3,241 -> 1,557/2,228/1,641 per frame, frame p50 9.95/9.55/10.0 -> 9.05/8.05/8.9 ms, p95 21.05/16.5/19.55 -> 14.75/11.65/13.45 ms. (cherry picked from commit b088dc46f7249705aa5e3fad8daaea4ec1acdd5d) * perf(core): the camera pass keeps its rig and camera walks while the engine's hierarchy holds still The animation-rate prep rebuilt its rig map (a full walk under every rate root) and the camera-part set (a walk under the camera) every frame, each walk one wrapper per node across the web engine boundary. The native engine already counts every add, remove, attach and destroy (Object3D::hierarchyVersion); it is now readable as the engine-internal getter __hierarchyRevision, and both walks run again only when it moves or the roots or their states change. Plain three has no count and walks every frame as before. Red-green: animation-rate 'keeps its rig and camera walks' fails on the old cull (3 walks expected, 4 seen), passes now; abi_test hierarchy_revision fails without the getter, passes with it; registry snapshot regenerated, --check 0; core 4241 pass, three-native 390 pass. Web Wasm 4,146,142 B (budget 4,194,304). Probe: 2 rebuilds in 600 calls on the shooter route (6 rigs). Shooter on the laptop (Iris Xe, --disable-gpu-vsync --disable-frame-rate-limit, load 5-9, 98 C), 3 pairs: cul…








What is this PR about?
A loop that builds one starter kit per game genre, then has a fresh "user" agent build a game from it. After each kit, the loop measures what went well and badly and fixes the authoring layer before the next kit.
Why does it matter?
Before the release, "make me a game like X" must give a good first build on few tokens. Nothing measures that today, and each kit lane learns on its own.
Plan (3 PRDs, one epic PR, merged at the release)
docs/kit-loop/ledger.json. The stop rule is the ranked genre list being done, or 2 iterations in a row with no KPI gain.pnpm perf:livebecamepnpm dashboard. It gets CI-speed, asset-perf and kit-loop tabs, and the two staticindex.htmlpages go.threenative-game-refsis renamed toThreeNativeHQ/threenative-reverse-engineer-mcp. The PRD adds the Tier-2 index and the Tier-1 classics in the loop's genre order.KPIs
Done
## Decisionssection.prd:progressexits 0 on all three, andpnpm check:docsexits 0.c2ab8c579,f799f51ab):pnpm dashboardis the one KPI hub; the two staticindex.htmlpages and the loops'report/servecommands are gone.8b8083e83): references come fromThreeNativeHQ/threenative-reverse-engineer-mcp; a "reverse engineer" request leads with the reference tools.f799f51ab):pnpm kit:loop record <dir>writes K1-K4 and the retro todocs/kit-loop/ledger.json; a missing input fails closed.e0b8e1867): the Tier-2 import (3,287 rows, byte-identical on a second import) and the OoT Tier-1 cards, data-repo validate green.a4d5345ef); the scaffold line says start the agent in the project, with a command-line engine call (42cbf971c,c5bc052e7);--scenariotakes{a,b}(bac933cdc); readAGENTS.mdonce (59d741907); review fixes (620e61917).55572d626): every shipped kit word routes "make me a game like " to its own kit; the core shim answers one engine call from outside the project and fails closed. With the per-fix specs above (frame-budget,scaffoldcompletion,servercommand line,bridgeClientbraces), each kit-loop win has a test that goes red if it regresses.Still open
pr563-followup. Kit findings for that lane: the 22 KB kitAGENTS.md, no win state at three Sunseeds, the wall-kick camera hides the hero.see PRD-750 Decisions). Rows 2–3 meet it (10.03, 12.59 ms); row 1 is over (17.27 ms). At row 3 all four KPIs meet their targets.pr563-followup(Starter kits: Unreal look, shooter revamp, performance by construction, kit contract and worker mode (PRD-723/746/747/748/749/752) #574). Retarget todevelopafter Starter kits: Unreal look, shooter revamp, performance by construction, kit contract and worker mode (PRD-723/746/747/748/749/752) #574 merges.Checklist
pnpm dashboardhub with CI speed, Asset perf and Kit loop tabs; PRD-751 is indone/pnpm kit:loop recordand the Kit loop tab